﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using StyleMVVM.Data.Collections;
using StyleMVVM.DependencyInjection;
using StyleMVVM.Utilities;
#if !DOT_NET
using Windows.Foundation.Collections;

#endif

namespace StyleMVVM.Data
{
	public enum EditingMode
	{
		ReadOnly,
		EditingWithRollBack,
		EidtingWithoutRollBack
	}

	/// <summary>
	/// A utiltity class that wraps a value of type T and records edits for an object
	/// </summary>
	/// <typeparam name="T">data object type to wrap</typeparam>
	[Export(typeof(IEditableDataWrapper<>))]
	public class EditableDataWrapper<T> : NotifyObject, IEditableDataWrapper<T> where T : class
	{
		private EditingMode editingMode;
		protected static List<string> properties;
		protected T dataValue;
		protected Dictionary<string, object> edits;

		public EditableDataWrapper()
		{
			init();
		}

		[Import]
		public IReflectionService ReflectionService { get; set; }

		public T Value
		{
			get { return dataValue; }
			set
			{
				if (dataValue != value)
				{
					var oldValue = dataValue;

					dataValue = value;

					ProcessValueChange(oldValue, dataValue);
				}
			}
		}

		public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
		{
			return new EditableDataWrapperEnumerator(this, properties);
		}

		IEnumerator IEnumerable.GetEnumerator()
		{
			return GetEnumerator();
		}

		public void Add(KeyValuePair<string, object> item)
		{
			ReflectionService.SetPropertyValue(Value, item.Key, item.Value);
		}

		public void Clear()
		{
			List<string> propertyEdits = new List<string>(edits.Keys);

			edits.Clear();

			foreach (string propertyEdit in propertyEdits)
			{
				OnPropertyChanged(propertyEdit);
			}
		}

		public bool Contains(KeyValuePair<string, object> item)
		{
			return properties.Contains(item.Key);
		}

		public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
		{
			throw new NotImplementedException();
		}

		public bool Remove(KeyValuePair<string, object> item)
		{
			return false;
		}

		public int Count
		{
			get { return properties.Count; }
		}

		public virtual bool IsReadOnly
		{
			get { return editingMode == EditingMode.ReadOnly; }
		}

		public void Add(string key, object value)
		{
			ReflectionService.SetPropertyValue(Value, key, value);
		}

		public bool ContainsKey(string key)
		{
			return properties.Contains(key);
		}

		public bool Remove(string key)
		{
			return false;
		}

		public virtual bool TryGetValue(string key, out object value)
		{
			value = null;

			if (properties.Contains(key))
			{
				if (!edits.TryGetValue(key, out value))
				{
					try
					{
						value = ReflectionService.GetPropertyValue(dataValue, key);

						return true;
					}
					catch (Exception exp)
					{
						Log.Error("Exception thrown while getting property value: " + key, exp);
					}
				}
			}

			return false;
		}

		public object this[string key]
		{
			get
			{
				object returnValue = null;

				TryGetValue(key, out returnValue);

				return returnValue;
			}
			set
			{
				if (editingMode != EditingMode.ReadOnly)
				{
					if (value != ReflectionService.GetPropertyValue(dataValue, key))
					{
						bool raisePendingChanges = edits.Count == 0;

						edits[key] = value;

						RaiseMapChanged(key);

						if (raisePendingChanges)
						{
							OnPropertyChanged("PendingChanges");
						}
					}
					else
					{
						if (edits.Remove(key))
						{
							RaiseMapChanged(key);

							if (edits.Count == 0)
							{
								OnPropertyChanged("PendingChanges");
							}
						}
					}
				}
			}
		}

		public ICollection<string> Keys
		{
			get
			{
				List<string> keys = new List<string>();

				foreach (var key in typeof(T).GetTypeInfo().DeclaredProperties)
				{
					// Only add properties with no index 
					if (key.GetIndexParameters().Length == 0)
					{
						keys.Add(key.Name);
					}
				}

				return keys;
			}
		}

		public ICollection<object> Values { get; private set; }

		public bool PendingChanges
		{
			get { return edits.Count > 0; }
		}

		public EditingMode EditingMode
		{
			get { return editingMode; }
			set
			{
				if (editingMode != value)
				{
					editingMode = value;

					OnPropertyChanged();
				}
			}
		}

		public event MapChangedEventHandler<string, object> MapChanged;

		public virtual void Commit()
		{
			foreach (string key in edits.Keys)
			{
				ReflectionService.SetPropertyValue(dataValue, key, edits[key]);
			}

			edits.Clear();

			OnPropertyChanged("PendingChanges");
		}

		public virtual void RollBack()
		{
			List<string> tempEdits =
				new List<string>(edits.Keys);

			edits.Clear();

			foreach (var edit in tempEdits)
			{
				RaiseMapChanged(edit);
			}

			OnPropertyChanged("PendingChanges");
		}

		protected void init()
		{
			edits = new Dictionary<string, object>();

			EditingMode = EditingMode.EditingWithRollBack;

			if (properties == null)
			{
				List<string> newProperties = new List<string>();

				foreach (var newProperty in typeof(T).GetTypeInfo().DeclaredProperties)
				{
					if (newProperty.GetIndexParameters().Length == 0)
					{
						newProperties.Add(newProperty.Name);
					}
				}

				properties = newProperties;
			}
		}

		protected virtual void ProcessValueChange(T oldValue, T newValue)
		{
			var oldPropertyChanged = oldValue as INotifyPropertyChanged;

			if (oldPropertyChanged != null)
			{
				oldPropertyChanged.PropertyChanged -=
					EditableDataWrapperPropertyChanged;
			}

			var newPropertyChanged = newValue as INotifyPropertyChanged;

			if (newPropertyChanged != null)
			{
				newPropertyChanged.PropertyChanged +=
					EditableDataWrapperPropertyChanged;
			}

			foreach (var key in Keys)
			{
				RaiseMapChanged(key);
			}
		}

		protected void RaiseMapChanged(string key,
		                               CollectionChange change = CollectionChange.ItemChanged)
		{
			if (MapChanged != null)
			{
#if NETFX_CORE
				MapChanged(this,
				           new ObservableDictionaryChangedEventArgs<string>(CollectionChange.ItemChanged, key));
#else
	// TODO
#endif
			}
		}

		private void EditableDataWrapperPropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			RaiseMapChanged(e.PropertyName);
		}

		public class EditableDataWrapperEnumerator : IEnumerator<KeyValuePair<string, object>>
		{
			protected EditableDataWrapper<T> wrapper;
			protected List<string> keys;
			protected IEnumerator<string> enumerator;

			public EditableDataWrapperEnumerator(EditableDataWrapper<T> wrapper, List<string> keys)
			{
				this.wrapper = wrapper;
				this.keys = keys;

				enumerator = keys.GetEnumerator();
			}

			public bool MoveNext()
			{
				return enumerator.MoveNext();
			}

			public void Reset()
			{
				enumerator.Reset();
			}

			public KeyValuePair<string, object> Current
			{
				get
				{
					return new KeyValuePair<string, object>(
						enumerator.Current, wrapper[enumerator.Current]);
				}
			}

			object IEnumerator.Current
			{
				get
				{
					return new KeyValuePair<string, object>(
						enumerator.Current, wrapper[enumerator.Current]);
				}
			}

			public void Dispose()
			{
				enumerator = null;
				wrapper = null;
				keys = null;
			}
		}
	}
}